feat: introduce ClickableRegion component to improve keyboard accessibility for interaction editors - #6094
Conversation
|
@AlexVelezLl, Should we add this to other interactions as well? |
|
Hi @Abhishek-Punhani! Yes, let's use this for the textEntry prompt, and I just merged the ordering interaction PR; we can refactor that editor as well! |
ae28f15 to
ba7e840
Compare
AlexVelezLl
left a comment
There was a problem hiding this comment.
Thanks @Abhishek-Punhani! It's really nice these button are finally accessible! I've found couple of things we might be able to simplify on the code.
| class="overlay-button" | ||
| :aria-label="ariaLabel" | ||
| @keydown.enter.prevent="onClick" | ||
| @keydown.space.prevent="onClick" |
There was a problem hiding this comment.
If we add @click, we don't need to specify these two, as for button elements, the space and enter keystrokes also fire the onClick handler on a button element.
| :minHeight="'80px'" | ||
| :autofocus="mode === 'edit' && isQuestionOpen" | ||
| :imageProcessor="EditorImageProcessor" | ||
| :tabindex="isQuestionOpen ? 0 : -1" |
There was a problem hiding this comment.
I don't think we need these tab indexes; if it's open, tabindex should also be -1 because we can already navigate through the editor to the input. (i.e., it should always be -1, as we are already handling the tab index in the outer container).
| function handlePromptClick(event) { | ||
| if (props.mode !== 'edit') return; | ||
| if (event.target.closest('button') || event.target.closest('input')) return; | ||
| const closestBtn = | ||
| event.target && event.target.closest ? event.target.closest('button') : null; | ||
| if (closestBtn && !closestBtn.classList.contains('overlay-button')) return; | ||
| const closestInput = | ||
| event.target && event.target.closest ? event.target.closest('input') : null; | ||
| if (closestInput) return; | ||
| if (!isQuestionOpen.value) { | ||
| event.stopPropagation(); | ||
| if (event && event.stopPropagation) event.stopPropagation(); | ||
| openQuestion(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Why do we need all of this? It's even less needed now because when the editor is already open, the Clickable region cannot fire any click event anymore, right?
| return { | ||
| borderColor: questionHasError.value ? tokens.error : tokens.fineLine, | ||
| cursor: props.mode === 'edit' ? 'pointer' : undefined, | ||
| '--clickable-region-hover-bg': palette.blue.v_100, |
There was a problem hiding this comment.
Hmm, I don't think we are using this anywhere.
| const closestBtn = | ||
| event.target && event.target.closest ? event.target.closest('button') : null; | ||
| if (closestBtn && !closestBtn.classList.contains('overlay-button')) return; | ||
| const closestInput = | ||
| event.target && event.target.closest ? event.target.closest('input') : null; | ||
| if (closestInput) return; | ||
| if (!isPromptOpen.value) { | ||
| event.stopPropagation(); | ||
| if (event && event.stopPropagation) event.stopPropagation(); | ||
| openPrompt(); | ||
| } | ||
| } | ||
|
|
||
| function handleItemClick(event, itemId) { | ||
| if (props.mode !== 'edit') return; | ||
| if (openItemId.value === itemId) return; | ||
| if (event.target.closest('button') || event.target.closest('input')) return; | ||
| event.stopPropagation(); | ||
| const closestBtn = | ||
| event.target && event.target.closest ? event.target.closest('button') : null; | ||
| if (closestBtn && !closestBtn.classList.contains('overlay-button')) return; | ||
| const closestInput = | ||
| event.target && event.target.closest ? event.target.closest('input') : null; | ||
| if (closestInput) return; | ||
| if (event && event.stopPropagation) event.stopPropagation(); |
| function handlePromptClick(event) { | ||
| if (props.mode !== 'edit') return; | ||
| if (event.target.closest('button') || event.target.closest('input')) return; | ||
| const closestBtn = | ||
| event.target && event.target.closest ? event.target.closest('button') : null; | ||
| if (closestBtn && !closestBtn.classList.contains('overlay-button')) return; | ||
| const closestInput = | ||
| event.target && event.target.closest ? event.target.closest('input') : null; | ||
| if (closestInput) return; |
ba7e840 to
5dc2938
Compare
|
@AlexVelezLl , Thanks for pointing this out. I’ve removed the redundant I also tested moving |
AlexVelezLl
left a comment
There was a problem hiding this comment.
Just a couple of things before merging!
|
|
||
| <div | ||
| class="clickable-area" | ||
| @click="onClick" |
There was a problem hiding this comment.
Just saw that this is similar to how we handle our KCard in KDS 😅, so yeah, that's fine. Could you add a comment here, please? We will introduce an a11y linter soon, and it will flag this as an incorrect pattern, but we should mute the linter instead, so it's best to add a clear comment explaining why a plain div has an @click handler without any tabindex/focus/space/enter management.
| event.target.closest( | ||
| 'button:not(.overlay-button), input, a, select, textarea, [role="button"]', |
There was a problem hiding this comment.
Oh, I meant to ask about this earlier 😅. Why is this needed? Right now, all elements that sit on top of this region have a .stop modifier in their implementation, so we shouldn't need something like this. We can add a comment specifying that interactive elements on top of it should implement a .stop modifier. This would be the same pattern we use for KCard.
| clickable | ||
| ? instance.proxy.$computedClass({ | ||
| ':hover': { backgroundColor: tokens.fineLine }, | ||
| }) |
There was a problem hiding this comment.
Could we use v-bind instead, similar to what you did for the TextEntry Editor? https://github.com/learningequality/studio/pull/6094/changes#diff-ce84be82b3009cec048a11c412c43d8ebe9a3055f682f745cb76d72afab21e80R462-R467
…bility for interaction editors Signed-off-by: Abhishek-Punhani <punhani.manavabhi@gmail.com>
5dc2938 to
1c96b33
Compare
AlexVelezLl
left a comment
There was a problem hiding this comment.
Thanks a lot @Abhishek-Punhani! LGTM!
Summary
Adding
ClickableRegioncomponent to improve keyboard accessibility for interaction editorsReferences
Closes #6043
Reviewer guidance
Navigate to the Qti-demo-page and and test navigating Choice Editor prompt/choice cards using keyboard (
Tab,Enter/Space) to verify accessiblearia-labelannouncements, focus outlines, and seamless interaction with nested controls (TipTap, selection inputs, action buttons) without triggering parent clicks.AI usage
Used Antigravity for final review and nitpicks.